package hooks

import (
	"context"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"strings"
	"time"

	log "atomgit.com/atomgit_dev/alog/alogcore"
	"atomgit.com/atomgit_dev/git-hooks/pkg/command"
	"atomgit.com/atomgit_dev/git-hooks/pkg/config"
	"atomgit.com/atomgit_dev/git-hooks/pkg/internal/agitext"
	"atomgit.com/atomgit_dev/git-hooks/pkg/streamio"
	pb "atomgit.com/atomgit_dev/satellite-proto/go"
)

// StartPackObjects hands over pack-objects logic to satellite.StartPackObjectsHookWithCache
func StartPackObjects(ctx context.Context, args []string) (int, error) {
	repository, err := agitext.GetPbRepositoryFromEnv()
	if err != nil {
		return 1, fmt.Errorf("fail to extract pb.Repository from env: %w", err)
	}

	if len(args) < 1 {
		return 1, fmt.Errorf("missing argument for pack-objects")
	}
	args = args[1:]

	hookWithGRPC := true
	var mode pb.PackObjectsHookWithCacheRequest_CacheMode
	switch config.Cfg.PackObjects.CacheMode {
	case "readonly":
		mode = pb.PackObjectsHookWithCacheRequest_READONLY
	case "readwrite":
		mode = pb.PackObjectsHookWithCacheRequest_READWRITE
	case "nocache":
		hookWithGRPC = false
	default:
		return 1, fmt.Errorf("invalid cache mode %q", config.Cfg.PackObjects.CacheMode)
	}

	fields := []log.Field{
		log.NewInt64Field("RepositoryID", repository.GlRepositoryId),
		log.NewStringField("args", strings.Join(args, " ")),
		log.NewStringField("mode", config.Cfg.PackObjects.CacheMode),
	}

	log.Info(ctx, "begin to execute pack-objects hook", fields...)

	// we intentionally make this timeout long enough for big packs
	ctx, cancel := context.WithTimeout(ctx, config.Cfg.PackObjects.Timeout)
	defer cancel()
	if !hookWithGRPC {
		cmd, err := command.New(ctx, exec.Command(config.Cfg.GitPath, args...), os.Stdin, os.Stdout, os.Stderr)
		if err != nil {
			return 1, fmt.Errorf("fail to start pack-objects: %w", err)
		}
		if err := cmd.Wait(); err != nil {
			return 1, fmt.Errorf("fail to run pack-objects: %w", err)
		}
		return 0, nil
	}

	conn, err := getSatelliteConn()
	if err != nil {
		return 1, fmt.Errorf("fail to get grpc conn: %w", err)
	}
	defer conn.Close()

	// add timeout to stream to avoid too much time consuming waiting cache response
	ctxStream, cancelStream := context.WithTimeout(ctx, config.Cfg.PackObjects.CacheWaitTimeout)
	defer cancelStream()
	stream, err := pb.NewHookServiceClient(conn).PackObjectsHookWithCache(ctxStream)
	if err != nil {
		return 1, fmt.Errorf("fail to create pack-objects hook stream: %w", err)
	}

	if err := stream.Send(&pb.PackObjectsHookWithCacheRequest{
		Repository: repository,
		Args:       args,
		Mode:       mode,
	}); err != nil {
		return 1, fmt.Errorf("fail to send pack-objects hook request: %w", err)
	}

	streamForRemote := streamio.NewWriter(func(p []byte) error {
		return stream.Send(&pb.PackObjectsHookWithCacheRequest{Stdin: p})
	})
	goon := make(chan struct{})
	magicStdin := NewBlockEOFBuffer(goon, io.TeeReader(os.Stdin, streamForRemote), 64*1024)
	magicStdout := &wrappedWriter{Writer: os.Stdout}
	magicStderr := &wrappedWriter{Writer: os.Stderr}
	cmd, err := command.New(ctx, exec.Command(config.Cfg.GitPath, args...), magicStdin, magicStdout, magicStderr)
	if err != nil {
		return 1, fmt.Errorf("fail to start pack-objects: %w", err)
	}

	<-magicStdin.Done()
	resp, err := stream.CloseAndRecv()
	if err != nil {
		log.WithError(err).Error(ctx, "fail to receive cache response, handle as cache miss", fields...)
	}

	isContinueCmd := false
	var fpCache *os.File
	if !resp.GetHit() {
		log.Info(ctx, "cache miss, continue to execute pack-objects", fields...)
		isContinueCmd = true
	} else {
		fields = append(fields, log.NewStringField("cache", resp.CacheFile))
		log.Info(ctx, "cache hit! try to accelerate using cache", fields...)
		fpCache, err = os.Open(resp.CacheFile)
		if err != nil {
			log.WithError(err).Error(ctx, "fail to open cache file, continue to run pack-objects", fields...)
			isContinueCmd = true
		} else {
			defer fpCache.Close()
		}
	}

	if isContinueCmd {
		// continue git-pack-objects
		close(goon)
		if err := cmd.Wait(); err != nil {
			return 1, fmt.Errorf("fail to run pack-objects: %w", err)
		}
		return 0, nil
	}

	magicStdout.Writer = ioutil.Discard
	magicStderr.Writer = ioutil.Discard
	// kill the blocking git-pack-objects command
	cancel()

	// Note(moweng.xx): `touch` the cache file before read is a good idea to record
	// the cache access time thus indicate whether the cache is still reusable, but
	// this would also involve some performance issues for rather `hot` caches. And
	// we never need so accurate access/modify time for cache managing.
	fInfo, err := fpCache.Stat()
	if err == nil && time.Since(fInfo.ModTime()) >= time.Hour*24 {
		log.Info(ctx, "touching cache to refresh atime", fields...)
		tAccess := time.Now()
		if err := os.Chtimes(resp.CacheFile, tAccess, tAccess); err != nil {
			log.WithError(err).Error(ctx, "fail to touch cache", fields...)
		}
	}

	fmt.Fprintf(os.Stderr, "%s\n", config.Cfg.PackObjects.CacheHitNotice)
	_, err = io.Copy(os.Stdout, fpCache)
	if err != nil {
		return 1, fmt.Errorf("fail to copy cache file: %w", err)
	}
	return 0, nil
}
